package library.http;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.webkit.URLUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.ref.WeakReference;
import java.lang.reflect.Type;
import java.net.Proxy;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import library.http.progress.HttpProgressHelper;
import library.http.ssl.SslUtils;
import okhttp3.Call;
import okhttp3.CertificatePinner;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

/**
 * Created by Mr.Fan on 2017/1/11.
 */

public class HttpThread implements Runnable, Cancelable {
    private static final int MSG_START = 0x11;
    private static final int MSG_CANCEL = 0x12;
    private static final int MSG_FINISH = 0x13;

    private static final ExecutorService executor = Executors.newCachedThreadPool();

    protected final HttpTask httpTask;
    private HttpCallback callback;

    private Call call;
    private boolean isCancelled = false;
    private String requestUrl;

    private Handler mHandler;
    private ResponseBody resp;

    public HttpThread(@NonNull HttpTask httpTask) {
        this.httpTask = httpTask;
    }

    public void printLog(Object arg) {
        if (httpTask != null && httpTask.isDebuggable()) {
            HttpUtils.debug(arg);
        }
    }

    public <T> void callback(HttpCallback<T> httpCall) {
        this.callback = httpCall;
        synchronized (executor) {
            executor.execute(this);
        }
    }

    public <T> T execute(Class<T> clz) throws Exception {
        httpTask.returnClass(clz);
        return (T) execute();
    }

    protected <T> T execute() throws Exception {
        Object result = doInBackground();
        if (result instanceof Exception) {
            throw (Exception) result;
        }
        return (T) result;
    }

    @Override
    public void cancel() {
        if (isCancelled)
            return;
        isCancelled = true;
        if (call != null) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (call != null) {
                            call.cancel();
                            call = null;
                        }
                    } catch (Exception ignore) {

                    }

                    try {
                        if (resp != null) {
                            resp.close();
                        }
                    } catch (Exception ignore) {

                    }
                }
            });
        }
    }

    private static final class ResponseHandler extends InnerHandler<HttpThread> {

        public ResponseHandler(HttpThread httpCall) {
            super(httpCall, Looper.getMainLooper());
        }

        @Override
        public void onHandle(Message msg, HttpThread thread) {

            switch (msg.what) {
                case MSG_START:
                    thread.onPreExecute();
                    break;
                case MSG_CANCEL:
                    thread.onCancelled();
                    break;
                case MSG_FINISH:
                    thread.onPostExecute(msg.obj);
                    break;
            }
        }
    }

    private synchronized Handler getHandler() {
        if (mHandler == null)
            mHandler = new ResponseHandler(this);
        return mHandler;
    }

    @Override
    public void run() {
        boolean isLooperPrepared = Looper.myLooper() != null;
        if (!isLooperPrepared) {
            try {
                Looper.prepare();
            } catch (Exception e) {

            }
        }

        {
            Message msg = getHandler().obtainMessage();
            msg.what = MSG_START;
            getHandler().sendMessage(msg);
        }
        final Object result = doInBackground();
        if (isCancelled) {
            Message msg = getHandler().obtainMessage();
            msg.what = MSG_CANCEL;
            msg.obj = result;
            getHandler().sendMessage(msg);
        } else {
            Message msg = getHandler().obtainMessage();
            msg.what = MSG_FINISH;
            msg.obj = result;
            getHandler().sendMessage(msg);

        }

        if (!isLooperPrepared) {
            try {
                Looper.loop();
            } catch (Exception e) {

            }
        }
    }

    public String parseUrl() {
        String url = HttpUtils.parseUrl(httpTask.getBaseUrl(), httpTask.getUrl());
        if (httpTask.getMethod() == null || httpTask.getMethod() == HttpMethod.GET) {
            url = HttpUtils.getFullGetUrl(url, httpTask.getParams(), httpTask.getQueries());
        }
        return url;
    }

    public Request request() {
        final String url = requestUrl = parseUrl();
        final Request.Builder builder = new Request.Builder();
        final HttpMethod method = httpTask.getMethod() != null ? httpTask.getMethod() : HttpMethod.GET;

        final FormBody.Builder formBuilder = httpTask.isFormUrlEncoded() ? new FormBody.Builder() : null;
        final MultipartBody.Builder multiBuilder = httpTask.isMultipart() ? new MultipartBody.Builder() : null;

        if (formBuilder != null && httpTask.getFields() != null) {
            for (Map.Entry<String, FormField> entry : httpTask.getFields().entrySet()) {
                String key = entry.getKey();
                FormField formField = entry.getValue();
                if (formField != null && formField.isEncoded())
                    formBuilder.addEncoded(key, formField.getValue());
                else
                    formBuilder.add(key, formField.getValue());
            }
        } else if (multiBuilder != null) {
            multiBuilder.setType(MediaType.parse(httpTask.getMultiType()));
            if (httpTask.getParts() != null) {
                for (Map.Entry<String, Object> entry : httpTask.getParts().entrySet()) {
                    String key = entry.getKey();
                    Object obj = entry.getValue();
                    if (obj instanceof FilePart) {
                        FilePart filePart = (FilePart) obj;
                        filePart.add2MultiBody(multiBuilder, key);
                    } else if (obj instanceof File) {
                        File file = (File) obj;
                        if (file != null && file.exists()) {
                            okhttp3.RequestBody fileBody = okhttp3.RequestBody.create(null, file);
                            multiBuilder.addFormDataPart(key, file.getName(), fileBody);
                        }
                    } else if (obj instanceof String) {
                        multiBuilder.addFormDataPart(key, (String) obj);
                    }
                }
            }
        }

        if (httpTask.getHeaders() != null) {
            for (Map.Entry<String, Object> entry : httpTask.getHeaders().entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value == null)
                    continue;
                builder.addHeader(key, String.valueOf(value));
            }
        }
        if (httpTask.getParams() != null && httpTask.getMethod() != HttpMethod.GET) {
            for (Map.Entry<String, Object> entry : httpTask.getParams().entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value == null)
                    continue;
                if (multiBuilder != null) {
                    multiBuilder.addFormDataPart(key, String.valueOf(value));
                } else if (formBuilder != null) {
                    formBuilder.add(key, String.valueOf(value));
                }
            }
        }

        if (httpTask.getCache() != null) {
            builder.header("Cache-Control", httpTask.getCache());
        }
        builder.url(url);
        if (formBuilder != null)
            builder.method(method.name(), formBuilder.build());
        else if (multiBuilder != null)
            builder.method(method.name(), multiBuilder.build());
        else
            builder.method(method.name(), null);
        return builder.build();
    }

    public Call call() {
        Request request = request();
        OkHttpClient client = httpTask.getHttpClient();
        OkHttpClient.Builder clientBuilder;
        if (client != null) {
            clientBuilder = client.newBuilder();
        } else {
            clientBuilder = new OkHttpClient.Builder();
        }

        if (httpTask.getSslConfig() != null) {
            SslUtils.configure(clientBuilder, httpTask.getSslConfig());
        }
        if (httpTask.getTimeoutConnect() > 0)
            clientBuilder.connectTimeout(httpTask.getTimeoutConnect(), TimeUnit.MILLISECONDS);
        if (httpTask.getTimeoutWrite() > 0)
            clientBuilder.writeTimeout(httpTask.getTimeoutWrite(), TimeUnit.MILLISECONDS);
        if (httpTask.getTimeoutRead() > 0)
            clientBuilder.readTimeout(httpTask.getTimeoutRead(), TimeUnit.MILLISECONDS);
        Proxy proxy = httpTask.getProxy() != null ? httpTask.getProxy() : client != null ? client.proxy() : null;
        clientBuilder.proxy(null);
        if (HttpUtils.isProxyAvailable(proxy)) {
            clientBuilder.proxy(proxy);
        }

        if (httpTask.getCertificatePinner() != null) {
            CertificatePinner.Builder builder = new CertificatePinner.Builder();
            for (KeyValue<String, String[]> kv : httpTask.getCertificatePinner()) {
                builder.add(kv.getKey(), kv.getValue());
            }
            clientBuilder.certificatePinner(builder.build());
        }
        Download<?> download = httpTask.getDownload();
        if (download != null && download.getProgress() != null) {
            HttpProgressHelper.addProgressDown(clientBuilder, download.crateProgressPublisher());
        }
        if (download != null && download.getRangeStart() > 0) {
            String range = download.getRangeEnd() > download.getRangeStart() ?
                    String.format("bytes=%d-%d", download.getRangeStart(), download.getRangeEnd())
                    :
                    String.format("bytes=%d-", download.getRangeStart());
            Request.Builder builder = request.newBuilder();
            builder.addHeader("Range", range);
            request = builder.build();
        }

        client = clientBuilder.build();
        return client.newCall(request);
    }

    protected void onPreExecute() {
        if (callback != null) {
            callback.onStart();
        }
    }

    protected Object doInBackground() {
        Object obj = null;
        try {
            this.call = call();
            printLog("Http Request @ url : " + getRequestUrl());
            Response response = call.execute();
            if (response.isSuccessful()) {
                resp = response.body();
                String contentType = resp.contentType() != null ? resp.contentType().toString() : null;
                Class returnClass = null;
                Type returnType = null;
                if (callback != null) {
                    returnClass = HttpUtils.getClassGenericClass(callback, 0);
                    returnType = HttpUtils.getClassGenericType(this, 0);
                }

                if (returnClass == null && httpTask.getReturnClass() != null) {
                    returnClass = httpTask.getReturnClass();
                }
                if (returnType == null && httpTask.getReturnType() != null) {
                    returnType = httpTask.getReturnType();
                }
                if (returnClass == String.class) {
                    obj = resp.string();
                } else if (returnClass == byte[].class) {
                    obj = resp.bytes();
                } else if (returnClass == ResponseBody.class) {
                    obj = resp;
                } else if (returnClass == File.class) {
                    if (httpTask.getDownload() != null) {
                        String defaultName = response.header("Content-Disposition", getRequestUrl());
                        String contentSize = response.header("Content-Length");
                        long contentLength = resp.contentLength();
                        if (contentLength < 0 && !TextUtils.isEmpty(contentSize) && TextUtils.isDigitsOnly(contentSize)) {
                            contentLength = Long.parseLong(contentSize);
                        }
                        Download download = httpTask.getDownload();
                        try {
                            File file = downloadFile(resp.byteStream(),
                                    download.getTarget(),
                                    defaultName,
                                    download.getRangeStart(),
                                    contentLength,
                                    download.isOverwrite());
                            obj = file;
                            printLog("Http Request Download @ file : " + file.getCanonicalPath());
                        } catch (Exception e) {
                            obj = new Exception("Download file failed for: " + e.getMessage());
                        }
                    } else {
                        obj = new Exception("Undefined 'Download' parameters.");
                    }
                } else if (returnClass != null && returnType != null) {
                    final Converter converter = getConverter(httpTask.getConverterList(), contentType, String.class, returnClass);

                    if (converter != null) {
                        obj = converter.convert(resp.string(), returnType);
                    } else
                        obj = new Exception("Unsupported callback parameterized type.");
                } else {
                    obj = new Exception("Undefined callback parameterized type.");
                }
            } else {
                obj = new Exception(response.code() + ":" + response.message());
            }
        } catch (SocketTimeoutException ste) {
            obj = ste;
        } catch (Throwable e) {
            obj = e;
        } finally {
        }
        return obj;
    }

    protected void onCancelled() {
        printLog(new SocketException("Http Request Cancelled @ url : \n" + getRequestUrl()));
        if (callback != null) {
            callback.onCancel();
            callback.onFinish();
            callback = null;
        }
    }

    private String getRequestUrl() {
        return requestUrl != null ? requestUrl : httpTask.getUrl();
    }

    protected void onPostExecute(Object result) {
        try {
            if (call != null) {
                call = null;
            }
            if (callback != null) {
                if (result instanceof Exception) {
                    Exception exception = (Exception) result;
                    if (exception instanceof SocketTimeoutException) {
                        callback.onTimeout(exception);
                    } else
                        callback.onFailure(exception);
                } else {
                    callback.onSuccess(result);
                }
            }
        } catch (Exception e) {
            if (callback != null) {
                try {
                    callback.onFailure(e);
                } catch (Exception ex) {
                    HttpUtils.debug(e);
                }
            } else {
                HttpUtils.debug(e);
            }
        } finally {
            if (callback != null) {
                callback.onFinish();
                callback = null;
            }
        }
    }

    protected Converter getConverter(List<Converter> list, String contentType, Class from, Class to) {
        Converter extend = null;
        if (list != null) {
            for (Converter converter : list) {
                if (converter == null)
                    continue;
                String[] contentTypes = converter.getContentTypes();
                boolean matchContentType = false;
                if (contentTypes != null) {
                    for (String type : contentTypes) {
                        if (String.valueOf(contentType).contains(String.valueOf(type))) {
                            matchContentType = true;
                            break;
                        }
                    }
                }
                if (!matchContentType) {
                    continue;
                }
                Class typeF = HttpUtils.getInterfaceGenericClass(converter, 0, 0);
                Class typeT = HttpUtils.getInterfaceGenericClass(converter, 0, 1);
                if (from != null && from == typeF) {
                    if (to == typeT) {
                        return converter;
                    } else if (extend == null && from.isAssignableFrom(typeF)) {
                        extend = converter;
                    }
                }
            }
        }
        return extend;
    }

    private static File downloadFile(final InputStream is,
                                     File target, String defaultName,
                                     final long range, final long length, final boolean overwrite) throws Exception {
        try {
            if (target == null)
                throw new IllegalArgumentException("没有指定目标文件存储");
            if (target.exists() && !overwrite) {
                throw new FileNotFoundException("文件已存在, 没有指定覆盖");
            }
            if (is.available() < 0 || length < 0)
                throw new IOException("无效的输入流.");
            File parent = target.isDirectory() ? target : target.getParentFile();
            if (range == 0) {
                if (!parent.exists() && !parent.mkdirs())
                    throw new FileNotFoundException("文件夹不存在, 创建新文件夹失败");
                if (!target.exists() && !target.createNewFile())
                    throw new FileNotFoundException("文件不存在, 创建新文件失败");
            }

            if (target.isDirectory()) {
                if (defaultName != null) {
                    try {
                        if (URLUtil.isNetworkUrl(defaultName)) {
                            HttpUrl httpUrl = HttpUrl.parse(defaultName);
                            String endpoint = httpUrl.encodedPath();
                            defaultName = endpoint.substring(endpoint.lastIndexOf("/") + 1);
                        } else {
                            Pattern pattern = Pattern.compile("^attachment; filename=\"([\\s\\S])\"$");
                            Matcher matcher = pattern.matcher(defaultName.trim());
                            if (matcher.find()) {
                                defaultName = matcher.group(1);
                            }
                        }
                    } catch (Exception e) {
                    }
                } else
                    defaultName = "unknown";
                target = new File(parent, defaultName);
            }
            File temp = new File(target + ".tmp");
            if (range > 0 && !temp.exists())
                throw new FileNotFoundException("缓存文件夹不存在, 无法继续下载");

            RandomAccessFile accessFile = new RandomAccessFile(temp, "rwd");
            accessFile.setLength(length);
            accessFile.seek(range);
            try {
                byte[] buffer = new byte[4096];
                int size;
                while ((size = is.read(buffer)) != -1) {
                    accessFile.write(buffer, 0, size);
                }
                if (temp.length() <= 0) {
                    throw new Exception("target is not available");
                }
            } finally {
                if (accessFile != null)
                    try {
                        accessFile.close();
                    } catch (Exception e) {

                    }
            }
            temp.renameTo(target);
        } finally {
            if (is != null)
                try {
                    is.close();
                } catch (Exception e) {

                }
        }
        return target;
    }


    private static abstract class InnerHandler<T> extends Handler {
        private final WeakReference<T> reference;

        public InnerHandler(T t) {
            reference = new WeakReference(t);
        }

        public InnerHandler(T t, Looper looper) {
            super(looper);
            reference = new WeakReference(t);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            T t = getTarget();
            if (t != null) {
                onHandle(msg, t);
            } else {
                removeCallbacksAndMessages(null);
            }
        }

        public T getTarget() {
            return reference.get();
        }

        public abstract void onHandle(Message msg, T t);
    }

}
